原文: https://v8.dev/blog/understanding-ecmascript-part-1
即使你了解JavaScript,阅读其语言规范,ECMAScript语言规范或简称为ECMAScript规范,也可能会感到相当艰巨。至少在我第一次阅读它时,就有这种感觉。
让我们从一个具体的例子开始,透过规范来理解它。以下的代码展示了Object.prototype.hasOwnProperty的使用:
const o = { foo: 1 };
o.hasOwnProperty('foo'); // true
o.hasOwnProperty('bar'); // false
在这个例子中,o
没有一个叫做hasOwnProperty
的属性,所以我们沿着原型链向上寻找它。我们在o的原型,也就是Object.prototype
中找到了它。
为了描述Object.prototype.hasOwnProperty
是如何工作的,规范使用了类似于伪代码的描述:
Object.prototype.hasOwnProperty(V)
当hasOwnProperty
方法被调用并带有参数V
时,将执行以下步骤:
P
be ? ToPropertyKey(V)
.O
be ? ToObject(this value)
.? HasOwnProperty(O, P)
.还有。。。
抽象操作HasOwnProperty
用于确定一个对象是否具有指定的属性键的自身属性。它将返回一个布尔值。此操作是带有参数O
和P
的调用,其中O
是对象,P
是属性键。这个抽象操作执行以下步骤:
Type(O)
is Object
.IsPropertyKey(P)
is true
.desc
be ? O.[[GetOwnProperty]](P)
.desc
is undefined
, return false
.true
.但是,什么是“抽象操作”? [[ ]]内部的东西是什么?为什么在函数前面有一个?符号?断言意味着什么呢?
让我们去寻找答案!
让我们从一些看起来熟悉的东西开始。规范使用了像undefined, true和false这样我们已经从JavaScript中了解的值。它们都是语言值,也是规范定义的语言类型的值。
规范也在内部使用语言值,例如,一个内部数据类型可能包含一个可能的值是true和false的字段。相比之下,JavaScript引擎通常不在内部使用语言值。例如,如果JavaScript引擎是用C++编写的,那么它通常会使用C++的true和false(而不是它对JavaScript的true和false的内部表示)。
除了语言类型,规范还使用规范类型,这些类型仅出现在规范中,而不是在JavaScript语言中。JavaScript引擎不需要(而且可以自由地)实现它们。在这篇博客文章中,我们将了解规范类型Record(及其子类型Completion Record)。
抽象操作是在ECMAScript规范中定义的函数;它们的定义目的是为了简洁地编写规范。JavaScript引擎不必在引擎内部将它们实现为单独的函数。它们不能直接从JavaScript调用。
内部插槽和内部方法使用 [[ ]] 包围。
内部插槽是一个JavaScript对象或规范类型的数据成员。它们用于存储对象的状态。内部方法是JavaScript对象的成员函数。
例如,每个JavaScript对象都有一个内部揽槽[[Prototype]]和一个内部方法[[GetOwnProperty]]。
内部插槽和方法不可从JavaScript访问。例如,你不能访问o.[[Prototype]]或调用o.[[GetOwnProperty]]。JavaScript引擎可以实现它们以供自己内部使用,但不一定要这样做。
(译者:这里还记得抽象操作和内部方法的区别吗?)
有时,内部方法会委派给同名的抽象操作,例如在普通对象的[[GetOwnProperty]]中:
[ [ GetOwnProperty ] ] (P)
当用属性键P调用O的[[GetOwnProperty]]内部方法时,将执行以下步骤:
返回 ! OrdinaryGetOwnProperty(O, P)。
(我们将在下一章中找出感叹号的含义。)
OrdinaryGetOwnProperty不是一个内部方法,因为它没有与任何对象相关联;相反,它操作的对象被作为一个参数传递。
OrdinaryGetOwnProperty被称为“ordinary”,因为它操作的是普通对象。ECMAScript对象可以是普通类型或者奇特类型。普通对象必须对一组被称为基本内部方法的方法具有默认行为。如果一个对象偏离了默认行为,那么它就是奇特的。
最知名的奇特对象是数组,因为它的length属性的行为方式不是默认的:设置length属性可以从数组中移除元素。
基本内部方法在这里.
那么问号和感叹号是什么呢?为了理解它们,我们需要研究一下完成记录!
完成记录是一个规范类型(仅为规范目的定义)。JavaScript引擎不需要有一个相应的内部数据类型。
完成记录是一个“记录”——一种具有固定的命名字段集的数据类型。完成记录有三个字段:
名字 | 描述 |
---|---|
[[Type]] |
normal 、break 、continue 、return 或throw 。除了normal 以外的所有类型都是突然完成。 |
[[Value]] |
完成发生时产生的值,例如,函数的返回值或异常(如果有的话)。 |
[[Target]] |
用于定向控制转移(与本文无关)。 |
每个抽象操作都隐式返回一个完成记录。即使看起来抽象操作会返回一个简单类型,例如Boolean,它也会被隐式地包装成一个带有normal类型的完成记录(参见隐式完成值)。
注意1:在这方面,规范并不完全一致;有一些辅助函数返回裸值,其返回值被直接使用,而不是从完成记录中提取值。这通常可以从上下文中清楚地看到。
注意2:规范编辑者 正在研究如何更明确地处理完成记录。
如果一个算法抛出一个异常,这意味着返回一个带有[[Type]]
throw和[[Value]]
是异常对象的完成记录。我们现在暂时忽略break、continue和return类型。
ReturnIfAbrupt(argument)意味着执行以下步骤:
也就是说,我们会检查一个完成记录;如果它是一个突然完成的记录,我们会立即返回。否则,我们会从完成记录中提取值。
ReturnIfAbrupt可能看起来像函数调用,但事实并非如此。它会导致出现ReturnIfAbrupt()的函数返回,并非ReturnIfAbrupt函数本身返回。它的行为更像C-like语言中的宏。
ReturnIfAbrupt可以这样使用:
现在问号起作用了:? Foo()相当于ReturnIfAbrupt(Foo())。使用简写是非常实用的:我们不需要每次都明确写出错误处理代码。
类似地,Let val be ! Foo()相当于:
利用这些知识,我们可以像这样重写Object.prototype.hasOwnProperty:
Object.prototype.hasOwnProperty(V)
1.Let P be ToPropertyKey(V).
2.If P is an abrupt completion, return P
3.Set P to P.[[Value]]
4.Let O be ToObject(this value).
5.If O is an abrupt completion, return O
6.Set O to O.[[Value]]
7.Let temp be HasOwnProperty(O, P).
8.If temp is an abrupt completion, return temp
9.Let temp be temp.[[Value]]
10.Return NormalCompletion(temp)
…我们也可以像这样重写 HasOwnProperty :
HasOwnProperty(O, P)
1.Assert: Type(O) is Object.
2.Assert: IsPropertyKey(P) is true.
3.Let desc be O.[[GetOwnProperty]](P).
4.If desc is an abrupt completion, return desc
5.Set desc to desc.[[Value]]
6.If desc is undefined, return NormalCompletion(false).
7.Return NormalCompletion(true).
我们也可以在没有感叹号的情况下重写[[GetOwnProperty]]内部方法:
O.[[GetOwnProperty]]
1.Let temp be OrdinaryGetOwnProperty(O, P).
2.Assert: temp is not an abrupt completion.
3.Let temp be temp.[[Value]].
4.Return NormalCompletion(temp).
在这里我们假设temp是一个全新的临时变量,不会与其他任何东西相冲突。
我们还获得了这样的知识,当一个return语句返回的东西不是一个Completion Record时,它会被隐式地包装在NormalCompletion之内。
规范使用了Return?Foo()这种符号——为什么使用问号?
Return ? Foo()展开为:
1. Let temp be Foo().
2. If temp is an abrupt completion, return temp.
3. Set temp to temp.[[Value]].
4. Return NormalCompletion(temp).
这与Return Foo()是相同的;对于突然的和正常的完成,它的行为方式是一样的。
只有出于编程方式上原因,才使用Return?Foo(),以使其更明确地表示Foo返回一个完成记录。
规范中的断言断定了算法的不变条件。它们是为了清晰逻辑而添加的,并不增加对实现的任何要求——实现不需要检查它们。
抽象操作委托给其他抽象操作(参见下图),但是基于此博客文章,我们应该能够明确它们的工作原理。我们会遇到属性描述符,这只是另一种规范类型。
Object.prototype.hasOwnProperty
的函数调用图
我们阅读了一个简单的方法——Object.prototype.hasOwnProperty——和它调用的抽象操作。熟悉了与错误处理相关的简写?和!。我们还了解了语言类型、规范类型、内部槽和内部方法。
评论
0